<?php

/**
 * Session类
 */

class Session extends Base {

	//生命时间，单位秒，这里默认30分钟
	protected $lifetime = 1800;
	
	//cookie缓存时间，默认0，表示关闭浏览器立即失效，可以设置大于0的数字，进行更长时间缓存
	protected $cookie_lifetime = 0;
	
	//cookie作用的目录
	protected $cookie_path = '/';
	
	//cookie作用的域名
	protected $cookie_domain = '';
	
	//cookie只通过https协议传输，默认FALSE
	protected $cookie_secure = FALSE;
	
	//cookie只通过http协议访问，默认FALSE。TRUE的话，javascript等将无法访问cookie,此设置可以有效地帮助减少身份盗窃，通过XSS攻击（虽然它不支持所有的浏览器）,一般我们设置FALSE
	protected $cookie_httponly = FALSE;
	
	//session键名前缀，比如前台和后台使用时，我们可以设置不同的前缀来区分开来
	protected $prefix = 'session_';
	
	//保存方式，database.mysql数据库，memcached.memcached服务器，default.默认$_SESSION
	protected $save_mode = 'default';
	
	//是否要匹配ip
	protected $match_ip = TRUE;
	
	//是否要匹配user_agent
	protected $match_user_agent = TRUE;
	
	//数据库链接
	protected $_db;
	
	//数据库地址
	protected $db_host = 'localhost';
	
	//数据库端口
	protected $db_port = 3306;
	
	//数据库用户
	protected $db_user = 'root';
	
	//数据库密码
	protected $db_pass = '';
	
	//数据库名称
	protected $db_name = 'angel';
	
	//数据库session表名称，里面包含字段session_id varchar(40), session_data text, last_access int(10)
	protected $db_table = 'angel_session';
	
	//memcached链接
	protected $_memcached;
	
	//memcached服务器配置
	protected $memcached_config = array(
		array(
			'host' => '127.0.0.1',
			'port' => 11211,
			'weight' => 1
		)
	);
	
	protected $_valid = array(
		'lifetime',
		'cookie_lifetime',
		'cookie_path',
		'cookie_domain',
		'cookie_secure',
		'cookie_httponly',
		'prefix',
		'save_mode',
		'match_ip',
		'match_user_agent',
		'db_host',
		'db_port',
		'db_user',
		'db_pass',
		'db_name',
		'db_table',
		'memcached_config'
	);

	public function __construct($config = array())
	{
		if ($config)
		{
			$this->config($config);
		}
	}
	
	/**
	 * 初始化
	 * @access public
	 * @return boolean
	 */
	public function init()
	{
		if (in_array($this->save_mode, array('database', 'memcached')))
		{
			//虚拟主机大多都不开启这个函数的
			if ( ! function_exists('session_set_save_handler'))
			{
				$this->set_error('函数session_set_save_handler不存在！');
				return FALSE;
			}
			
			//初始化相关服务器链接
			$init_fun = 'init_'.$this->save_mode;
			
			if ( ! $this->$init_fun())
			{
				return FALSE;
			}
			
			session_set_save_handler(
				array($this, '_open'),
				array($this, '_close'),
				array($this, '_read'),
				array($this, '_write'),
				array($this, '_destroy'),
				array($this, '_gc')
			);
		}
		
		//开启session
		session_start();
		
		//首先检查生命周期
		if ($this->is_expired())
		{
			$this->destroy();
		}
		
		//如果开启ip匹配，这里判断是否匹配ip
		if ($this->match_ip && ! $this->is_match_ip())
		{
			$this->destroy();
		}
		
		//如果开启用户代理匹配，这里判断是否匹配user_agent
		if ($this->match_user_agent && ! $this->is_match_user_agent())
		{
			$this->destroy();
		}
		
		//销毁已经闪出的数据
		$this->flash_sweep();
		
		//标记闪出数据已经闪出
		$this->flash_mark();
		
		//修改最近访问时间
		$this->set('last_access', time());
		
		return TRUE;
	}
	
	/**
	 * 初始化数据库
	 * @access protected
	 * @return boolean
	 */
	protected function init_database()
	{
		//链接数据库
		$this->_db = @mysql_connect($this->db_host.':'.$this->db_port, $this->db_user, $this->db_pass);
		//是否连上数据库表
		if ($this->_db)
		{
			if (FALSE == mysql_select_db($this->db_name, $this->_db))
			{
				$this->set_error('数据库不存在！');
				return FALSE;
			}
			return TRUE;
		}
		else
		{
			$this->set_error('连接数据库主机失败！');
			return FALSE;
		}
	}
	
	/**
	 * 初始化memcached
	 * @access protected
	 * @return boolean
	 */
	protected function init_memcached()
	{
		if (class_exists('Memcached'))
		{
			$this->_memcached = new Memcached();
		}
		elseif (class_exists('Memcache'))
		{
			$this->_memcached = new Memcache();
		}
		else
		{
			$this->set_error('Memcached或Memcache扩展未找到！');
			return FALSE;
		}
		
		if (get_class($this->_memcached) == 'Memcached')
		{
			//Memcached
			foreach ($this->memcached_config as $config)
			{
				if ( ! $this->_memcached->addServer($config['host'], $config['port'], $config['weight']))
				{
					$this->set_error('向连接池中添加memcached服务器失败！');
					return FALSE;
				}
			}
		}
		else
		{
			//Memcache
			foreach ($this->memcached_config as $config)
			{
				if ( ! $this->_memcached->addServer($config['host'], $config['port'], 0, $config['weight']))
				{
					$this->set_error('向连接池中添加memcache服务器失败！');
					return FALSE;
				}
			}
		}
		
		return TRUE;
	}
	
	/**
	 * 写入session
	 * @access public
	 * @param $name string session名称
	 * @param $value mixed session值
	 * @param $flash boolean 是否定义为闪出
	 * @return void
	 */
	public function set($name, $value, $flash = FALSE)
	{	
		if (strpos($name, '_new_')!==0 && strpos($name, '_old_')!==0)
		{
			$name = $this->prefix.$name;
			$name = $flash ? ('_new_'.$name) : $name;
		}
		
		$_SESSION[$name] = $value;
	}
	
	/**
	 * 返回session
	 * @access public
	 * @param $name
	 * @param $flash boolean 是否闪出
	 * @return mixed
	 */
	public function get($name, $flash = FALSE)
	{
		if ($flash)
		{
			$name = '_old_'.$this->prefix.$name;
		}
		else{
			$name = $this->prefix.$name;
		}

		return isset($_SESSION[$name]) ? $_SESSION[$name] : FALSE;
	}

	/**
	 * 删除
	 * @access public
	 * @param $name string session名称
	 * @return void
	 */
	public function delete($name)
	{
		if (strpos($name, '_new_') !== 0 && strpos($name, '_old_') !== 0)
		{
			$name = $this->prefix.$name;
		}
		
		if (isset($_SESSION[$name]))
		{
			unset($_SESSION[$name]);
		}
	}
	
	/**
	 * 彻彻底底的销毁
	 * @access public
	 * @return void
	 */
	public function destroy()
	{
		session_destroy();
		
		//删除掉cookie值
		if (isset($_COOKIE[session_name()]))
		{
			unset($_COOKIE[session_name()]);
			setcookie(session_name(), '', time() - 42000, $this->cookie_path, $this->cookie_domain, $this->cookie_secure, $this->cookie_httponly);
		}
		
		session_start();
	}

	/**
	 * 设置闪出session
	 * @access public
	 * @param $name string session名称
	 * @param $value mixed session值
	 * @return void
	 */
	public function set_flash($name, $value)
	{
		$this->set($name, $value, TRUE);
	}
	
	/**
	 * 返回闪出session
	 * @access public
	 * @param $name string session名称
	 * @return mixed
	 */
	public function get_flash($name)
	{
		return $this->get($name, TRUE);
	}
	
	/**
	 * 将闪出session持久化
	 * @access public
	 * @return void
	 */
	public function keep_flash($name)
	{
		if (isset($_SESSION['_old_'.$this->prefix.$name]))
		{
			$this->set($name, $_SESSION['_old_'.$this->prefix.$name]);
			$this->delete('_old_'.$this->prefix.$name);
		}
	}

	/**
	 * 标记闪出数据
	 * @access protected
	 * @return void
	 */
	protected function flash_mark()
	{
		//把_new_标记的改为_old_标记，待下次清除
		foreach ($_SESSION as $name => $value)
		{
			$arr = explode('_new_', $name);
			if (is_array($arr) && count($arr) == 2)
			{
				$new_name = '_old_'.$arr[1];
				$this->set($new_name, $value);
				$this->delete($name);
			}
		}
	}
	
	/**
	 * 销毁闪出数据
	 * @access protected
	 * @return void
	 */
	protected function flash_sweep()
	{
		//删除_old_标记的
		foreach ($_SESSION as $name => $value)
		{
			if (strpos($name, '_old_') === 0)
			{
				$this->delete($name);
			}
		}
	}
	
	/**
	 * 判断是否过期
	 * @access protected
	 * @return boolean
	 */
	protected function is_expired()
	{
		if ( ! $this->get('last_access'))
		{
			return TRUE;
		}
		
		return ($this->lifetime < (time() - $this->get('last_access'))) ? TRUE : FALSE;
	}
	
	/**
	 * 判断是否匹配ip
	 * @access protected
	 * @return boolean
	 */
	protected function is_match_ip()
	{
		if ( ! $this->get('ip_address'))
		{
			$this->set('ip_address', $this->get_ip());
			return TRUE;
		}
		
		return ($this->get_ip() == $this->get('ip_address')) ? TRUE : FALSE;
	}
	
	/**
	 * 判断是否匹配user_agent
	 * @access protected
	 * @return boolean
	 */
	protected function is_match_user_agent()
	{
		$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
		
		if ( ! $this->get('user_agent'))
		{
			$this->set('user_agent', $user_agent);
			return TRUE;
		}
		
		return ($user_agent == $this->get('user_agent')) ? TRUE : FALSE;
	}
	
	/**
	 * 返回客户端ip
	 * @access public
	 * @return string
	 */
	public function get_ip()
	{
		static $ip;
		
		if (isset($ip))
		{
			return $ip;
		}
		
		if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
		{
			$arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
			$pos =  array_search('unknown', $arr);
			if (false !== $pos)
			{
				unset($arr[$pos]);
			}
			$ip   =  trim($arr[0]);
		}
		elseif (isset($_SERVER['HTTP_CLIENT_IP']))
		{
			$ip = $_SERVER['HTTP_CLIENT_IP'];
		}
		elseif (isset($_SERVER['REMOTE_ADDR']))
		{
			$ip = $_SERVER['REMOTE_ADDR'];
		}
		
		return $ip;
	}
	
	public function set_lifetime($lifetime)
	{
		$this->lifetime = intval($lifetime) >= 0 ? intval($lifetime) : $this->lifetime;
		return $this;
	}
	
	public function set_cookie_lifetime($cookie_lifetime)
	{
		$this->cookie_lifetime = intval($cookie_lifetime) >= 0 ? intval($cookie_lifetime) : $this->cookie_lifetime;
		ini_set('session.cookie_lifetime', $this->cookie_lifetime);
		return $this;
	}
	
	public function set_cookie_path($cookie_path)
	{
		$this->cookie_path = trim($cookie_path);
		ini_set('session.cookie_path', $this->cookie_path);
		return $this;
	}
	
	public function set_cookie_domain($cookie_domain)
	{
		$this->cookie_domain = trim($cookie_domain);
		ini_set('session.cookie_domain', $this->cookie_domain);
		return $this;
	}
	
	public function set_cookie_secure($cookie_secure)
	{
		$this->cookie_secure = (boolean)$cookie_secure;
		ini_set('session.cookie_secure', $this->cookie_secure);
		return $this;
	}
	
	public function set_cookie_httponly($cookie_httponly)
	{
		$this->cookie_httponly = (boolean)$cookie_httponly;
		ini_set('session.cookie_httponly', $this->cookie_httponly);
		return $this;
	}
	
	public function set_match_ip($match_ip)
	{
		$this->match_ip = (boolean)$match_ip;
		return $this;
	}
	
	public function set_match_user_agent($match_user_agent)
	{
		$this->match_user_agent = (boolean)$match_user_agent;
		return $this;
	}
	
	public function set_db_port($db_port)
	{
		$this->db_port = intval($db_port);
		return $this;
	}
	
	public function set_memcached_config($memcached_config)
	{
		$this->memcached_config = array();
		
		if ($memcached_config)
		{
			foreach ($memcached_config as $c)
			{
				if (isset($c['host']) && isset($c['port']) && isset($c['weight']))
				{
					$this->memcached_config[] = $c;
				}
			}
		}
		
		return $this;
	}

	//================================================================================================================
	
	/**
	 * 打开链接
	 * @access public
	 * @return boolean
	 */
	public function _open()
	{
		return TRUE;
	}
	
	/**
	 * 关闭链接
	 * @access public
	 * @return boolean
	 */
	public function _close()
	{
		return TRUE;
	}
	
	/**
	 * 读取
	 * @access public
	 * @param $id string 键值
	 * @return string
	 */
	public function _read($id)
	{
		$method = '_read_'.$this->save_mode;
		return $this->$method($id);
	}
	
	/**
	 * 从数据库读取
	 * @access protected
	 * @param $id string 键值
	 * @return string
	 */
	protected function _read_database($id)
	{
		if (function_exists('mysql_real_escape_string'))
		{
			$id = mysql_real_escape_string($id, $this->_db);
		}
		else
		{
			$id = mysql_escape_string($id);
		}
		
		$sql = "SELECT `session_data` FROM `".$this->db_table."` where `session_id` = '".$id."'";
		$result = mysql_query($sql, $this->_db);
		if ($result && mysql_num_rows($result))
		{
			$record = mysql_fetch_assoc($result);
			return $record['session_data'];
		}
		return '';
	}
	
	/**
	 * 从memcached读取
	 * @access protected
	 * @param $id string 键值
	 * @return string
	 */
	protected function _read_memcached($id)
	{
		$data = $this->_memcached->get($id);
		return (FALSE !== $data) ? $data : '';
	}
	
	/**
	 * 写入
	 * @access public
	 * @param $id string 键值
	 * @param $data string 数据
	 * @return boolean
	 */
	public function _write($id, $data)
	{
		$access = time();
		$method = '_write_'.$this->save_mode;
		return $this->$method($id, $data, $access);
		
	}
	
	/**
	 * 写入数据库
	 * @access protected
	 * @param $id string 键值
	 * @param $data string 数据
	 * @return boolean
	 */
	protected function _write_database($id, $data, $access)
	{
		if (function_exists('mysql_real_escape_string'))
		{
			$id = mysql_real_escape_string($id, $this->_db);
			$data = mysql_real_escape_string($data, $this->_db);
		}
		else
		{
			$id = mysql_escape_string($id);
			$data = mysql_escape_string($data);
		}
		
		$sql = "REPLACE INTO `".$this->db_table."` (`session_id`, `session_data`, `last_access`) VALUES ('".$id."', '".$data."', ".$access.")";
		return (boolean)mysql_query($sql, $this->_db);
	}
	
	/**
	 * 写入memcached
	 * @access protected
	 * @param $id string 键值
	 * @param $data string 数据
	 * @return boolean
	 */
	protected function _write_memcached($id, $data, $access)
	{
		return $this->_memcached->set($id, $data, $this->lifetime);
	}
	
	/**
	 * 销毁
	 * @access public
	 * @param $id string 键值
	 * @return boolean
	 */
	public function _destroy($id)
	{
		$method = '_destroy_'.$this->save_mode;
		return $this->$method($id);
	}
	
	/**
	 * 从数据库销毁
	 * @access protected
	 * @param $id string 键值
	 * @return boolean
	 */
	protected function _destroy_database($id)
	{
		if (function_exists('mysql_real_escape_string'))
		{
			$id = mysql_real_escape_string($id, $this->_db);
		}
		else
		{
			$id = mysql_escape_string($id);
		}
		
		$sql = "DELETE FROM `".$this->db_table."` WHERE `session_id` = '".$id."'";
		return (boolean)mysql_query($sql, $this->_db);
	}
	
	/**
	 * 从memcached销毁
	 * @access protected
	 * @param $id string 键值
	 * @return boolean
	 */
	protected function _destroy_memcached($id)
	{
		return $this->_memcached->delete($id);
	}
	
	/**
	 * 垃圾回收，这个玩意的确没必要了，手动删除的好
	 * @access public
	 * @param $max integer 最大时间
	 * @return boolean
	 */
	public function _gc($max)
	{
		return TRUE;
	}

}